/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.support.scripting;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.script.Bindings;
import javax.script.ScriptContext;

import fr.x9c.cadmium.kernel.Value;
import fr.x9c.cadmium.util.InputStreamFromReader;
import fr.x9c.cadmium.util.PrintStreamToWriter;

/**
 * This class implements a context for OCaml scripting.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.1
 * @since 1.1
 */
public final class OCamlContext implements ScriptContext {

    /** Handled scopes. */
    private static final List<Integer> SCOPES =
        Collections.unmodifiableList(Arrays.asList(new Integer[] {
                                                       Integer.valueOf(ENGINE_SCOPE),
                                                       Integer.valueOf(GLOBAL_SCOPE)
                                                   }));

    /** Global bindings. */
    private static OCamlBindings globalScope = null;

    /** Engine bindings. */
    private OCamlBindings engineScope;

    /** Standard output stream for script. */
    private PrintStream stdOut;

    /** Standard error stream for script. */
    private PrintStream stdErr;

    /** Standard input stream for script. */
    private InputStream stdIn;

    /** Standard output stream for script, as a writer. */
    private Writer writer;

    /** Standard error stream for script, as a writer. */
    private Writer errorWriter;

    /** Standard input stream for script, as a reader. */
    private Reader reader;

    /**
     * Constructs a context from streams.
     * @param out standard output stream for script
     * @param err standard error stream for script
     * @param in standard input stream for script
     */
    public OCamlContext(final PrintStream out,
                        final PrintStream err,
                        final InputStream in) {
        assert out != null : "null out";
        assert err != null : "null err";
        assert in != null : "null in";
        this.engineScope = new OCamlBindings();
        this.stdOut = out;
        this.stdErr = err;
        this.stdIn = in;
        this.writer = new PrintWriter(out, true);
        this.errorWriter = new PrintWriter(err, true);
        this.reader = new InputStreamReader(in);
    } // end constructor(PrintStream, PrintStream, InputStream)

    /**
     * Constructs a context, with default streams.
     */
    public OCamlContext() {
        this(System.out, System.err, System.in);
    } // end empty constructor

    /**
     * Copy constructor, with specific bindings.
     * @param that instance to copy - should not be <tt>null</tt>
     * @param b bindings replacing those of the copied instance - should not be <tt>null</tt>
     */
    public OCamlContext(final OCamlContext that,
                        final OCamlBindings b) {
        assert that != null : "null that";
        assert b != null : "null b";
        this.globalScope = that.globalScope;
        this.engineScope = b;
        this.stdOut = that.stdOut;
        this.stdErr = that.stdErr;
        this.stdIn = that.stdIn;
        this.writer = that.writer;
        this.errorWriter = that.errorWriter;
        this.reader = that.reader;
    } // end constructor(OCamlContext, OCamlBindings)

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getAttribute(final String name)
        throws IllegalArgumentException, NullPointerException {
        if (name == null) {
            throw new NullPointerException("null name");
        } // end if
        if (this.engineScope.containsKey(name)) {
            return this.engineScope.get(name);
        } else if ((OCamlContext.globalScope != null)
                   && OCamlContext.globalScope.containsKey(name)) {
            return OCamlContext.globalScope.get(name);
        } else {
            return null;
        } // end if/elsif/else
    } // end method 'getAttribute(String)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getAttribute(final String name, final int scope)
        throws IllegalArgumentException, NullPointerException {
        if (name == null) {
            throw new NullPointerException("null name");
        } // end if
        switch (scope) {
        case ENGINE_SCOPE:
            return this.engineScope.get(name);
        case GLOBAL_SCOPE:
            if (OCamlContext.globalScope != null) {
                return OCamlContext.globalScope.get(name);
            } else {
                return null;
            } // end if/else
        default:
            throw new IllegalArgumentException("invalid scope");
        } // end switch
    } // end method 'getAttribtue(String, int)'

    /**
     * {@inheritDoc}
     */
    @Override
    public int getAttributesScope(final String name)
        throws NullPointerException {
        if (name == null) {
            throw new NullPointerException("null name");
        } // end if
        if (this.engineScope.containsKey(name)) {
            return ENGINE_SCOPE;
        } else if ((OCamlContext.globalScope != null)
                   && OCamlContext.globalScope.containsKey(name)) {
            return GLOBAL_SCOPE;
        } else {
            return -1;
        } // end if/elsif/else
    } // end method 'getAttributesScope(String)'

    /**
     * Returns a map containing all bindings (global and engine ones),
     * globals bindings being superseded by engine ones with the same name.
     * @return a map containing all bindings
     */
    Map<String, Value> getBindings() {
        final Map<String, Value> res = new HashMap<String, Value>();
        if (this.globalScope != null) {
            for (String key : this.globalScope.keySet()) {
                res.put(key, this.globalScope.getValue(key));
            } // end for
        } // end if
        for (String key : this.engineScope.keySet()) {
            res.put(key, this.engineScope.getValue(key));
        } // end for
        return res;
    } // end method 'getBindings()'

    /**
     * {@inheritDoc}
     */
    @Override
    public Bindings getBindings(final int scope)
        throws IllegalArgumentException {
        switch (scope) {
        case ENGINE_SCOPE:
            return this.engineScope;
        case GLOBAL_SCOPE:
            return OCamlContext.globalScope;
        default:
            throw new IllegalArgumentException("invalid scope");
        } // end switch
    } // end method 'getBindings(int)'

    /**
     * Returns the stream used for standard input.
     * @return the stream used for standard input
     */
    InputStream getStdIn() {
        return this.stdIn;
    } // end method 'getStdIn()'

    /**
     * Returns the stream used for standard output.
     * @return the stream used for standard output
     */
    PrintStream getStdOut() {
        return this.stdOut;
    } // end method 'getStdOut()'

    /**
     * Returns the stream used for standard error.
     * @return the stream used for standard error
     */
    PrintStream getStdErr() {
        return this.stdErr;
    } // end method 'getStdErr()'

    /**
     * {@inheritDoc}
     */
    @Override
    public Writer getErrorWriter() {
        return this.errorWriter;
    } // end method 'getErrorWriter()'

    /**
     * {@inheritDoc}
     */
    @Override
    public Reader getReader() {
        return this.reader;
    } // end method 'getReader()'

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Integer> getScopes() {
        return OCamlContext.SCOPES;
    } // end method 'getScopes()'

    /**
     * {@inheritDoc}
     */
    @Override
    public Writer getWriter() {
        return this.writer;
    } // end method 'getWriter()'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object removeAttribute(final String name, final int scope)
        throws IllegalArgumentException, NullPointerException {
        if (name == null) {
            throw new NullPointerException("null name");
        } // end if
        switch (scope) {
        case ENGINE_SCOPE:
            return this.engineScope.remove(name);
        case GLOBAL_SCOPE:
            if (OCamlContext.globalScope != null) {
                return OCamlContext.globalScope.remove(name);
            } else {
                return null;
            } // end if/else
        default:
            throw new IllegalArgumentException("invalid scope");
        } // end switch
    } // end method 'removeAttribute(String, int)'

    /**
     * {@inheritDoc}
     */
    @Override
    public void setAttribute(final String name,
                             final Object value,
                             final int scope)
        throws IllegalArgumentException, NullPointerException {
        if (name == null) {
            throw new NullPointerException("null name");
        } // end if
        switch (scope) {
        case ENGINE_SCOPE:
            this.engineScope.put(name, value);
            break;
        case GLOBAL_SCOPE:
            if (OCamlContext.globalScope != null) {
                OCamlContext.globalScope.put(name, value);
            } // end if
            break;
        default:
            throw new IllegalArgumentException("invalid scope");
        } // end switch
    } // end method 'setAttribute(String, Object, int)'

    /**
     * {@inheritDoc}
     */
    @Override
    public void setBindings(final Bindings bindings,
                            final int scope)
        throws IllegalArgumentException, NullPointerException {
        switch (scope) {
        case ENGINE_SCOPE:
            if (bindings == null) {
                throw new NullPointerException("null bindings for engine scope");
            } // end if
            this.engineScope = OCamlBindings.instance(bindings);
            break;
        case GLOBAL_SCOPE:
            OCamlContext.globalScope = OCamlBindings.instance(bindings);
            break;
        default:
            throw new IllegalArgumentException("invalid scope");
        } // end switch
    } // end method 'setBindings(Bindings, int)'

    /**
     * {@inheritDoc}
     */
    @Override
    public void setErrorWriter(final Writer writer) {
        if (writer != null) {
            this.errorWriter = writer;
            this.stdErr = new PrintStreamToWriter(writer);
        } // end if
    } // end method 'setErrorWriter(Writer)'

    /**
     * {@inheritDoc}
     */
    @Override
    public void setReader(final Reader reader) {
        if (reader != null) {
            this.reader = reader;
            this.stdIn = new InputStreamFromReader(reader);
        } // end if
    } // end method 'setReader(Reader)'

    /**
     * {@inheritDoc}
     */
    @Override
    public void setWriter(final Writer writer) {
        if (writer != null) {
            this.writer = writer;
            this.stdOut = new PrintStreamToWriter(writer);
        } // end if
    } // end method 'setWriter(Writer)'

    /**
     * Returns an instance that is either the passed parameter if it is an
     * instance of <tt>OCamlContext</tt>, or a new instance otherwise.
     * @param sc script context - should not be <tt>null</tt>
     * @return an <tt>OCamlContext</tt> instance of the passed context
     */
    static OCamlContext instance(final ScriptContext sc) {
        assert sc != null : "null sc";
        if (sc instanceof OCamlContext) {
            return (OCamlContext) sc;
        } else {
            final OCamlContext res = new OCamlContext();
            res.setErrorWriter(sc.getErrorWriter());
            res.setReader(sc.getReader());
            res.setWriter(sc.getWriter());
            res.setBindings(sc.getBindings(ENGINE_SCOPE), ENGINE_SCOPE);
            res.setBindings(sc.getBindings(GLOBAL_SCOPE), GLOBAL_SCOPE);
            return res;
        } // end if/else
    } // end method 'instance(ScriptContext)'

} // end class 'OCamlContext'
